﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Properties;

namespace BorgExt
{
    public class TypeBuilder
    {
        private int _nextContractId = 1;
        private int _nextSlotDefinitionId = 1;
        private int _nextParamDefinitionId = 1;
        private int _nextPluginId = 1;
        private int _nextExtensionTypeId = 1;
        private int _nextPlugTypeId = 1;
        private int _nextSlotTypeId = 1;
        private int _nextParamId = 1;

        private Runtime Runtime { get; set; }

        protected internal TypeBuilder(Runtime runtime)
        {
            Runtime = runtime;
        }

        public Contract CreateContract(string name, Assembly assembly,
            ICollection<SlotDefinition> slotDefinitions)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");

            var uri = new Uri(assembly.EscapedCodeBase);
            var lastWriteTime = File.GetLastWriteTime(uri.LocalPath);
            
            return InternalCreateContract(NextContractId(), name, assembly, uri, lastWriteTime, slotDefinitions);
        }

        public Contract CreateContract(string name, Assembly assembly,
            DateTime lastModified, ICollection<SlotDefinition> slotDefinitions)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");

            var uri = new Uri(assembly.EscapedCodeBase);
            
            return InternalCreateContract(NextContractId(), name, assembly, uri, lastModified, slotDefinitions);
        }

        public Contract CreateContract(string name, Uri uri, DateTime lastModified,
            ICollection<SlotDefinition> slotDefinitions)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (uri == null)
                throw new ArgumentNullException("uri");
            
            return InternalCreateContract(NextContractId(), name, null, uri, lastModified, slotDefinitions);
        }

        public SlotDefinition CreateSlotDefinition(SlotDefinitionAttribute attribute,
            string typeName, ICollection<ParamDefinition> paramDefinitions)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (typeName == null)
                throw new ArgumentNullException("typeName");

            if (typeName == string.Empty)
                throw new ArgumentException(Messages.Type_name_must_not_be_empty, "typeName");

            if (string.IsNullOrEmpty(attribute.Name))
            {
                var index = typeName.LastIndexOf('.');

                attribute.Name = index < 0
                    ? typeName
                    : typeName.Substring(index + 1);
            }

            return InternalCreateSlotDefinition(NextSlotDefinitionId(), attribute, typeName, null, paramDefinitions);
        }

        public SlotDefinition CreateSlotDefinition(SlotDefinitionAttribute attribute,
            Type type, ICollection<ParamDefinition> paramDefinitions)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (type == null)
                throw new ArgumentNullException("type");

            if (string.IsNullOrEmpty(attribute.Name))
                attribute.Name = type.Name;

            return InternalCreateSlotDefinition(NextSlotDefinitionId(), attribute, type.FullName, type, paramDefinitions);
        }

        public ParamDefinition CreateParamDefinition(ParamDefinitionAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateParamDefinition(NextParamDefinitionId(), attribute);
        }

        public Plugin CreatePlugin(string name, Assembly assembly, ICollection<ExtensionType> extensionTypes)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");

            var uri = new Uri(assembly.EscapedCodeBase);
            var lastWriteTime = File.GetLastWriteTime(uri.LocalPath);
            
            return InternalCreatePlugin(NextPluginId(), name, assembly, uri, lastWriteTime, extensionTypes);
        }

        public Plugin CreatePlugin(string name, Assembly assembly,
            DateTime lastModified, ICollection<ExtensionType> extensionTypes)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");

            var uri = new Uri(assembly.EscapedCodeBase);
            
            return InternalCreatePlugin(NextPluginId(), name, assembly, uri, lastModified, extensionTypes);
        }

        public Plugin CreatePlugin(string name, Uri uri,
            DateTime lastModified, ICollection<ExtensionType> extensionTypes)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Messages.Name_must_not_be_empty, "name");

            if (uri == null)
                throw new ArgumentNullException("uri");
            
            return InternalCreatePlugin(NextPluginId(), name, null, uri, lastModified, extensionTypes);
        }

        public ExtensionType CreateExtensionType(ExtensionAttribute attribute,
            string typeName, ICollection<PlugType> plugTypes, ICollection<SlotType> slotTypes)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (typeName == null)
                throw new ArgumentNullException("typeName");

            if (typeName == string.Empty)
                throw new ArgumentException(Messages.Type_name_must_not_be_empty, "typeName");

            if (string.IsNullOrEmpty(attribute.Name))
            {
                var index = typeName.LastIndexOf('.');
                
                attribute.Name = index < 0
                    ? typeName
                    : typeName.Substring(index + 1);
            }

            return InternalCreateExtensionType(NextExtensionTypeId(), attribute, typeName, null, plugTypes, slotTypes);
        }

        public ExtensionType CreateExtensionType(ExtensionAttribute attribute, Type type,
            ICollection<PlugType> plugTypes, ICollection<SlotType> slotTypes)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (type == null)
                throw new ArgumentNullException("type");

            if (string.IsNullOrEmpty(attribute.Name))
                attribute.Name = type.Name;

            return InternalCreateExtensionType(NextExtensionTypeId(), attribute, type.FullName, type, plugTypes, slotTypes);
        }

        public PlugType CreatePlugType(PlugAttribute attribute, IEnumerable<Param> parameters)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreatePlugType(NextPlugTypeId(), attribute, parameters);
        }

        public Param CreateParam(ParamAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateParam(NextParamId(), attribute);
        }

        public SlotType CreateSlotType(SlotAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateSlotType(NextSlotTypeId(), attribute);
        }

        private Contract InternalCreateContract(int id, string name, Assembly assembly,
            Uri uri, DateTime lastModified, IEnumerable<SlotDefinition> slotDefinitions)
        {
            var runtimeContract = new Contract(id, name, assembly, uri, lastModified) {runtime = Runtime};
            var contract = runtimeContract;
            
            if (slotDefinitions != null)
            {
                var hashSet = new HashSet<string>();
                
                foreach (var slotDef in slotDefinitions)
                {
                    if (hashSet.Contains(slotDef.Name))
                        throw new ArgumentException("SlotDefinition name not unique", "slotDefinitions");

                    hashSet.Add(slotDef.Name);

                    if (slotDef.Contract != null)
                        throw new InvalidOperationException("SlotDefinition is already assigned to a Contract");

                    slotDef.Contract = contract;
                }

                contract.SlotDefinitions = new MetaElementCollection<SlotDefinition>(slotDefinitions);
            }
            else
                contract.SlotDefinitions = new MetaElementCollection<SlotDefinition>();

            return contract;
        }

        private static SlotDefinition InternalCreateSlotDefinition(int id,
            SlotDefinitionAttribute attribute, string typeName,
            Type type, IEnumerable<ParamDefinition> paramDefinitions)
        {
            var slotDef = new SlotDefinition(id, attribute.Name)
            {
                TypeName = typeName,
                Type = type
            };

            if (paramDefinitions != null)
            {
                var hashSet = new HashSet<string>();

                foreach (var paramDef in paramDefinitions)
                {
                    if (hashSet.Contains(paramDef.Name))
                        throw new ArgumentException("ParamDefinition name not unique", "paramDefinitions");

                    hashSet.Add(paramDef.Name);

                    if (paramDef.SlotDefinition != null)
                        throw new InvalidOperationException("ParamDefinition is already assigned to a SlotDefinition");

                    paramDef.SlotDefinition = slotDef;
                }

                slotDef.ParamDefinitions = new MetaElementCollection<ParamDefinition>(paramDefinitions);
            }
            else
                slotDef.ParamDefinitions = new MetaElementCollection<ParamDefinition>();

            return slotDef;
        }

        private ParamDefinition InternalCreateParamDefinition(int id, ParamDefinitionAttribute attribute)
        {
            var paramDef = new ParamDefinition(id, attribute.Name)
            {
                Type = attribute.Type
            };

            if (attribute.DefaultValue != ParamDefinition.NoDefault)
                paramDef.DefaultParam = new Param(NextParamId(), attribute.Name)
                {
                    Value = attribute.DefaultValue
                };

            return paramDef;
        }

        private Plugin InternalCreatePlugin(int id, string name, Assembly assembly, Uri uri,
            DateTime lastModified, IEnumerable<ExtensionType> extensionTypes)
        {
            var runtimePlugin = new Plugin(id, name, assembly, uri, lastModified) {runtime = Runtime};
            var plugin = runtimePlugin;

            if (extensionTypes != null)
            {
                var hashSet = new HashSet<string>();

                foreach (var extType in extensionTypes)
                {
                    if (hashSet.Contains(extType.Name))
                        throw new ArgumentException("ExtensionType name not unique", "extensionTypes");

                    hashSet.Add(extType.Name);

                    if (extType.Plugin != null)
                        throw new InvalidOperationException("ExtensionType is already assigned to a Plugin");

                    extType.Plugin = plugin;
                }

                plugin.ExtensionTypes = new MetaElementCollection<ExtensionType>(extensionTypes);
            }
            else
                plugin.ExtensionTypes = new MetaElementCollection<ExtensionType>();

            return plugin;
        }

        private static ExtensionType InternalCreateExtensionType(int id, ExtensionAttribute attribute,
            string typeName, Type type, IEnumerable<PlugType> plugTypes, IEnumerable<SlotType> slotTypes)
        {
            var extType = new ExtensionType(id, attribute.Name)
            {
                TypeName = typeName,
                Type = type
            };

            if (!string.IsNullOrEmpty(attribute.OnCreated))
                extType.OnCreated = new EventHandlerInfo(attribute.OnCreated);

            if (!string.IsNullOrEmpty(attribute.OnDestroyed))
                extType.OnDestroyed = new EventHandlerInfo(attribute.OnDestroyed);

            if (plugTypes != null)
            {
                var hashSet = new HashSet<string>();

                foreach (var plugType in plugTypes)
                {
                    if (hashSet.Contains(plugType.Name))
                        throw new ArgumentException("PlugType name not unique", "plugTypes");

                    hashSet.Add(plugType.Name);

                    if (plugType.ExtensionType != null)
                        throw new InvalidOperationException("PlugType is already assigned to a ExtensionType");

                    plugType.ExtensionType = extType;
                }

                extType.PlugTypes = new MetaElementCollection<PlugType>(plugTypes);
            }
            else
                extType.PlugTypes = new MetaElementCollection<PlugType>();

            if (slotTypes != null)
            {
                var hashSet = new HashSet<string>();

                foreach (var slotType in slotTypes)
                {
                    if (hashSet.Contains(slotType.Name))
                        throw new ArgumentException("SlotType name not unique", "slotTypes");

                    hashSet.Add(slotType.Name);
                    if (slotType.ExtensionType != null)
                        throw new InvalidOperationException("SlotType is already assigned to a ExtensionType");

                    slotType.ExtensionType = extType;
                }

                extType.SlotTypes = new MetaElementCollection<SlotType>(slotTypes);
            }
            else
                extType.SlotTypes = new MetaElementCollection<SlotType>();

            if (!string.IsNullOrEmpty(attribute.OnCreated))
                extType.OnCreated = new EventHandlerInfo(attribute.OnCreated);

            if (!string.IsNullOrEmpty(attribute.OnDestroyed))
                extType.OnDestroyed = new EventHandlerInfo(attribute.OnDestroyed);

            return extType;
        }

        private static PlugType InternalCreatePlugType(int id, PlugAttribute attribute, IEnumerable<Param> parameters)
        {
            var plugType = new PlugType(id, attribute.Name)
            {
                AutoPlug = attribute.AutoPlug,
                AutoSelect = attribute.AutoSelect
            };

            if (!string.IsNullOrEmpty(attribute.OnPlugged))
                plugType.OnPlugged = new EventHandlerInfo(attribute.OnPlugged);

            if (!string.IsNullOrEmpty(attribute.OnSelected))
                plugType.OnSelected = new EventHandlerInfo(attribute.OnSelected);

            if (!string.IsNullOrEmpty(attribute.OnUnplugged))
                plugType.OnUnplugged = new EventHandlerInfo(attribute.OnUnplugged);

            if (!string.IsNullOrEmpty(attribute.OnDeselected))
                plugType.OnDeselected = new EventHandlerInfo(attribute.OnDeselected);

            if (parameters != null)
            {
                var hashSet = new HashSet<string>();

                foreach (var parameter in parameters)
                {
                    if (hashSet.Contains(parameter.Name))
                        throw new ArgumentException("Parameter name not unique", "parameters");

                    hashSet.Add(parameter.Name);

                    if (parameter.PlugType != null)
                        throw new InvalidOperationException("Parameter is already assigned to a PlugType");

                    parameter.PlugType = plugType;
                }

                plugType.Params = new MetaElementCollection<Param>(parameters);
            }
            else
                plugType.Params = new MetaElementCollection<Param>();

            if (!string.IsNullOrEmpty(attribute.OnPlugged))
                plugType.OnPlugged = new EventHandlerInfo(attribute.OnPlugged);

            if (!string.IsNullOrEmpty(attribute.OnSelected))
                plugType.OnSelected = new EventHandlerInfo(attribute.OnSelected);

            if (!string.IsNullOrEmpty(attribute.OnUnplugged))
                plugType.OnUnplugged = new EventHandlerInfo(attribute.OnUnplugged);

            if (!string.IsNullOrEmpty(attribute.OnDeselected))
                plugType.OnDeselected = new EventHandlerInfo(attribute.OnDeselected);

            return plugType;
        }

        private static Param InternalCreateParam(int id, ParamAttribute attribute)
        {
            return new Param(id, attribute.Name)
            {
                Value = attribute.Value
            };
        }

        private static SlotType InternalCreateSlotType(int id, SlotAttribute attribute)
        {
            var slotType = new SlotType(id, attribute.Name)
            {
                AutoOpen = attribute.AutoOpen,
                AutoPlug = attribute.AutoPlug,
                AutoSelect = attribute.AutoSelect,
                Shared = attribute.Shared
            };

            if (!string.IsNullOrEmpty(attribute.OnOpened))
                slotType.OnOpened = new EventHandlerInfo(attribute.OnOpened);

            if (!string.IsNullOrEmpty(attribute.OnPlugged))
                slotType.OnPlugged = new EventHandlerInfo(attribute.OnPlugged);

            if (!string.IsNullOrEmpty(attribute.OnSelected))
                slotType.OnSelected = new EventHandlerInfo(attribute.OnSelected);

            if (!string.IsNullOrEmpty(attribute.OnClosed))
                slotType.OnClosed = new EventHandlerInfo(attribute.OnClosed);

            if (!string.IsNullOrEmpty(attribute.OnUnplugged))
                slotType.OnUnplugged = new EventHandlerInfo(attribute.OnUnplugged);

            if (!string.IsNullOrEmpty(attribute.OnDeselected))
                slotType.OnDeselected = new EventHandlerInfo(attribute.OnDeselected);

            return slotType;
        }

        private int NextContractId()
        {
            return _nextContractId++;
        }

        private int NextSlotDefinitionId()
        {
            return _nextSlotDefinitionId++;
        }

        private int NextParamDefinitionId()
        {
            return _nextParamDefinitionId++;
        }

        private int NextPluginId()
        {
            return _nextPluginId++;
        }

        private int NextExtensionTypeId()
        {
            return _nextExtensionTypeId++;
        }

        private int NextPlugTypeId()
        {
            return _nextPlugTypeId++;
        }

        private int NextParamId()
        {
            return _nextParamId++;
        }

        private int NextSlotTypeId()
        {
            return _nextSlotTypeId++;
        }
    }
}

